/************************************************************************
 *
 *   Copyright (c) 2018 PX4 Development Team. All rights reserved.
 *   Copyright (c) 2015 - 2016, Freescale Semiconductor, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * LICENSE NOTE FOR EXTERNAL LIBOPENCM3 LIBRARY:
 *
 *   The PX4 development team considers libopencm3 to be
 *   still GPL, not LGPL licensed, as it is unclear if
 *   each and every author agreed to the LGPS -> GPL change.
 *
 ***********************************************************************/

/**
 * @file cdcacm.c
 * @author David Sidrane <david_s5@nscdg.com>
 */
#include "hw_config.h"

#include <stdlib.h>
#include <stdint.h>

#include "bl.h"

#include "fsl_device_registers.h"

#include <stdio.h>
#include <stdlib.h>

#include "usb_device_config.h"
#include "usb.h"
#include "usb_device.h"

#include "usb_device_class.h"
#include "usb_device_cdc_acm.h"
#include "usb_device_ch9.h"

#include "usb_device_descriptor.h"
#include "virtual_com.h"
#if defined(FSL_FEATURE_USB_KHCI_KEEP_ALIVE_ENABLED) && (FSL_FEATURE_USB_KHCI_KEEP_ALIVE_ENABLED > 0U) && \
    defined(USB_DEVICE_CONFIG_KEEP_ALIVE_MODE) && (USB_DEVICE_CONFIG_KEEP_ALIVE_MODE > 0U) &&             \
    defined(FSL_FEATURE_USB_KHCI_USB_RAM) && (FSL_FEATURE_USB_KHCI_USB_RAM > 0U)
#include "fsl_smc.h"
#endif
/*******************************************************************************
* Definitions
******************************************************************************/
#include "usb_device_descriptor.c"


/* USB clock source and frequency*/
#define USB_FS_CLK_SRC kCLOCK_UsbSrcIrc48M
#define USB_FS_CLK_FREQ 48000000U

/*******************************************************************************
 * Prototypes
 ******************************************************************************/

usb_status_t USB_DeviceCdcVcomCallback(class_handle_t handle, uint32_t event, void *param);
usb_status_t USB_DeviceCallback(usb_device_handle handle, uint32_t event, void *param);

/*******************************************************************************
* Variables
******************************************************************************/
static bool g_usb_initalized = false;
volatile bool g_usb_tx_done = true;
extern usb_device_endpoint_struct_t g_UsbDeviceCdcVcomDicEndpoints[];
extern usb_device_class_struct_t g_UsbDeviceCdcVcomConfig;
/* Data structure of virtual com device */
static usb_cdc_vcom_struct_t s_cdcVcom;

/* Line codinig of cdc device */
static uint8_t s_lineCoding[LINE_CODING_SIZE] = {
	/* E.g. 0x00,0xC2,0x01,0x00 : 0x0001C200 is 115200 bits per second */
	(LINE_CODING_DTERATE >> 0U) & 0x000000FFU,
	(LINE_CODING_DTERATE >> 8U) & 0x000000FFU,
	(LINE_CODING_DTERATE >> 16U) & 0x000000FFU,
	(LINE_CODING_DTERATE >> 24U) & 0x000000FFU,
	LINE_CODING_CHARFORMAT,
	LINE_CODING_PARITYTYPE,
	LINE_CODING_DATABITS
};

/* Abstract state of cdc device */
static uint8_t s_abstractState[COMM_FEATURE_DATA_SIZE] = {(STATUS_ABSTRACT_STATE >> 0U) & 0x00FFU,
							  (STATUS_ABSTRACT_STATE >> 8U) & 0x00FFU
							 };

/* Country code of cdc device */
static uint8_t s_countryCode[COMM_FEATURE_DATA_SIZE] = {(COUNTRY_SETTING >> 0U) & 0x00FFU,
							(COUNTRY_SETTING >> 8U) & 0x00FFU
						       };

/* CDC ACM information */
static usb_cdc_acm_info_t s_usbCdcAcmInfo USB_DATA_ALIGN = {{0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, 0, 0, 0, 0, 0} ;
/* Data buffer for receiving and sending*/
static uint8_t s_currRecvBuf[DATA_BUFF_SIZE] USB_DATA_ALIGN;
volatile static int32_t s_recvSize = 0;

/* USB device class information */
static usb_device_class_config_struct_t s_cdcAcmConfig[1] = {{
		USB_DeviceCdcVcomCallback, 0, &g_UsbDeviceCdcVcomConfig,
	}
};

/* USB device class configuraion information */
static usb_device_class_config_list_struct_t s_cdcAcmConfigList = {
	s_cdcAcmConfig, USB_DeviceCallback, 1,
};

#if defined(FSL_FEATURE_USB_KHCI_KEEP_ALIVE_ENABLED) && (FSL_FEATURE_USB_KHCI_KEEP_ALIVE_ENABLED > 0U) && \
    defined(USB_DEVICE_CONFIG_KEEP_ALIVE_MODE) && (USB_DEVICE_CONFIG_KEEP_ALIVE_MODE > 0U) &&             \
    defined(FSL_FEATURE_USB_KHCI_USB_RAM) && (FSL_FEATURE_USB_KHCI_USB_RAM > 0U)
volatile static uint8_t s_waitForDataReceive = 0;
volatile static uint8_t s_comOpen = 0;
#endif
/*******************************************************************************
* Code
******************************************************************************/
/*!
 * @brief CDC class specific callback function.
 *
 * This function handles the CDC class specific requests.
 *
 * @param handle          The CDC ACM class handle.
 * @param event           The CDC ACM class event type.
 * @param param           The parameter of the class specific request.
 *
 * @return A USB error code or kStatus_USB_Success.
 */
usb_status_t USB_DeviceCdcVcomCallback(class_handle_t handle, uint32_t event, void *param)
{
	uint32_t len;
	uint16_t *uartBitmap;
	usb_device_cdc_acm_request_param_struct_t *acmReqParam;
	usb_device_endpoint_callback_message_struct_t *epCbParam;
	usb_status_t error = kStatus_USB_Error;
	usb_cdc_acm_info_t *acmInfo = &s_usbCdcAcmInfo;
	acmReqParam = (usb_device_cdc_acm_request_param_struct_t *)param;
	epCbParam = (usb_device_endpoint_callback_message_struct_t *)param;

	switch (event) {
	case kUSB_DeviceCdcEventSendResponse: {
			if ((epCbParam->length != 0) && (!(epCbParam->length % g_UsbDeviceCdcVcomDicEndpoints[0].maxPacketSize))) {
				/* If the last packet is the size of endpoint, then send also zero-ended packet,
				 ** meaning that we want to inform the host that we do not have any additional
				 ** data, so it can flush the output.
				 */
				error = USB_DeviceCdcAcmSend(handle, USB_CDC_VCOM_BULK_IN_ENDPOINT, NULL, 0);

			} else if ((epCbParam->buffer != NULL) || ((epCbParam->buffer == NULL) && (epCbParam->length == 0))) {
				/* User: add your own code for send complete event */
				/* Schedule buffer for next receive event */
				g_usb_tx_done = true;
				error = USB_DeviceCdcAcmRecv(handle, USB_CDC_VCOM_BULK_OUT_ENDPOINT, s_currRecvBuf,
							     g_UsbDeviceCdcVcomDicEndpoints[0].maxPacketSize);
#if defined(FSL_FEATURE_USB_KHCI_KEEP_ALIVE_ENABLED) && (FSL_FEATURE_USB_KHCI_KEEP_ALIVE_ENABLED > 0U) && \
    defined(USB_DEVICE_CONFIG_KEEP_ALIVE_MODE) && (USB_DEVICE_CONFIG_KEEP_ALIVE_MODE > 0U) &&             \
    defined(FSL_FEATURE_USB_KHCI_USB_RAM) && (FSL_FEATURE_USB_KHCI_USB_RAM > 0U)
				s_waitForDataReceive = 1;
				USB0->INTEN &= ~USB_INTEN_SOFTOKEN_MASK;
#endif
			}
		}
		break;

	case kUSB_DeviceCdcEventRecvResponse: {

			s_recvSize = epCbParam->length;

			if (s_recvSize > 0) {
				for (int i = 0; i < s_recvSize; i++) {
					buf_put(s_currRecvBuf[i]);
				}

				s_recvSize = 0;
			}


#if defined(FSL_FEATURE_USB_KHCI_KEEP_ALIVE_ENABLED) && (FSL_FEATURE_USB_KHCI_KEEP_ALIVE_ENABLED > 0U) && \
    defined(USB_DEVICE_CONFIG_KEEP_ALIVE_MODE) && (USB_DEVICE_CONFIG_KEEP_ALIVE_MODE > 0U) &&             \
    defined(FSL_FEATURE_USB_KHCI_USB_RAM) && (FSL_FEATURE_USB_KHCI_USB_RAM > 0U)
			s_waitForDataReceive = 0;
			USB0->INTEN |= USB_INTEN_SOFTOKEN_MASK;
#endif

			if (s_recvSize == 0) {
				/* Schedule buffer for next receive event */
				error = USB_DeviceCdcAcmRecv(handle, USB_CDC_VCOM_BULK_OUT_ENDPOINT, s_currRecvBuf,
							     g_UsbDeviceCdcVcomDicEndpoints[0].maxPacketSize);
#if defined(FSL_FEATURE_USB_KHCI_KEEP_ALIVE_ENABLED) && (FSL_FEATURE_USB_KHCI_KEEP_ALIVE_ENABLED > 0U) && \
    defined(USB_DEVICE_CONFIG_KEEP_ALIVE_MODE) && (USB_DEVICE_CONFIG_KEEP_ALIVE_MODE > 0U) &&             \
    defined(FSL_FEATURE_USB_KHCI_USB_RAM) && (FSL_FEATURE_USB_KHCI_USB_RAM > 0U)
				s_waitForDataReceive = 1;
				USB0->INTEN &= ~USB_INTEN_SOFTOKEN_MASK;
#endif
			}
		}
		break;

	case kUSB_DeviceCdcEventSerialStateNotif:
		((usb_device_cdc_acm_struct_t *)handle)->hasSentState = 0;
		error = kStatus_USB_Success;
		break;

	case kUSB_DeviceCdcEventSendEncapsulatedCommand:
		break;

	case kUSB_DeviceCdcEventGetEncapsulatedResponse:
		break;

	case kUSB_DeviceCdcEventSetCommFeature:
		if (USB_DEVICE_CDC_FEATURE_ABSTRACT_STATE == acmReqParam->setupValue) {
			if (1 == acmReqParam->isSetup) {
				*(acmReqParam->buffer) = s_abstractState;

			} else {
				*(acmReqParam->length) = 0;
			}

		} else if (USB_DEVICE_CDC_FEATURE_COUNTRY_SETTING == acmReqParam->setupValue) {
			if (1 == acmReqParam->isSetup) {
				*(acmReqParam->buffer) = s_countryCode;

			} else {
				*(acmReqParam->length) = 0;
			}

		} else {
		}

		error = kStatus_USB_Success;
		break;

	case kUSB_DeviceCdcEventGetCommFeature:
		if (USB_DEVICE_CDC_FEATURE_ABSTRACT_STATE == acmReqParam->setupValue) {
			*(acmReqParam->buffer) = s_abstractState;
			*(acmReqParam->length) = COMM_FEATURE_DATA_SIZE;

		} else if (USB_DEVICE_CDC_FEATURE_COUNTRY_SETTING == acmReqParam->setupValue) {
			*(acmReqParam->buffer) = s_countryCode;
			*(acmReqParam->length) = COMM_FEATURE_DATA_SIZE;

		} else {
		}

		error = kStatus_USB_Success;
		break;

	case kUSB_DeviceCdcEventClearCommFeature:
		break;

	case kUSB_DeviceCdcEventGetLineCoding:
		*(acmReqParam->buffer) = s_lineCoding;
		*(acmReqParam->length) = LINE_CODING_SIZE;
		error = kStatus_USB_Success;
		break;

	case kUSB_DeviceCdcEventSetLineCoding: {
			if (1 == acmReqParam->isSetup) {
				*(acmReqParam->buffer) = s_lineCoding;

			} else {
				*(acmReqParam->length) = 0;
			}
		}

		error = kStatus_USB_Success;
		break;

	case kUSB_DeviceCdcEventSetControlLineState: {
			s_usbCdcAcmInfo.dteStatus = acmReqParam->setupValue;

			/* activate/deactivate Tx carrier */
			if (acmInfo->dteStatus & USB_DEVICE_CDC_CONTROL_SIG_BITMAP_CARRIER_ACTIVATION) {
				acmInfo->uartState |= USB_DEVICE_CDC_UART_STATE_TX_CARRIER;

			} else {
				acmInfo->uartState &= (uint16_t)~USB_DEVICE_CDC_UART_STATE_TX_CARRIER;
			}

			/* activate carrier and DTE */
			if (acmInfo->dteStatus & USB_DEVICE_CDC_CONTROL_SIG_BITMAP_DTE_PRESENCE) {
				acmInfo->uartState |= USB_DEVICE_CDC_UART_STATE_RX_CARRIER;

			} else {
				acmInfo->uartState &= (uint16_t)~USB_DEVICE_CDC_UART_STATE_RX_CARRIER;
			}

			/* Indicates to DCE if DTE is present or not */
			acmInfo->dtePresent = (acmInfo->dteStatus & USB_DEVICE_CDC_CONTROL_SIG_BITMAP_DTE_PRESENCE) ? true : false;

			/* Initialize the serial state buffer */
			acmInfo->serialStateBuf[0] = NOTIF_REQUEST_TYPE;                /* bmRequestType */
			acmInfo->serialStateBuf[1] = USB_DEVICE_CDC_NOTIF_SERIAL_STATE; /* bNotification */
			acmInfo->serialStateBuf[2] = 0x00;                              /* wValue */
			acmInfo->serialStateBuf[3] = 0x00;
			acmInfo->serialStateBuf[4] = 0x00; /* wIndex */
			acmInfo->serialStateBuf[5] = 0x00;
			acmInfo->serialStateBuf[6] = UART_BITMAP_SIZE; /* wLength */
			acmInfo->serialStateBuf[7] = 0x00;
			/* Notifiy to host the line state */
			acmInfo->serialStateBuf[4] = acmReqParam->interfaceIndex;
			/* Lower byte of UART BITMAP */
			uartBitmap = (uint16_t *)&acmInfo->serialStateBuf[NOTIF_PACKET_SIZE + UART_BITMAP_SIZE - 2];
			*uartBitmap = acmInfo->uartState;
			len = (uint32_t)(NOTIF_PACKET_SIZE + UART_BITMAP_SIZE);

			if (0 == ((usb_device_cdc_acm_struct_t *)handle)->hasSentState) {
				error = USB_DeviceCdcAcmSend(handle, USB_CDC_VCOM_INTERRUPT_IN_ENDPOINT, acmInfo->serialStateBuf, len);

				if (kStatus_USB_Success != error) {
					usb_echo("kUSB_DeviceCdcEventSetControlLineState error!");
				}

				((usb_device_cdc_acm_struct_t *)handle)->hasSentState = 1;
			}

			/* Update status */
			if (acmInfo->dteStatus & USB_DEVICE_CDC_CONTROL_SIG_BITMAP_CARRIER_ACTIVATION) {
				/*  To do: CARRIER_ACTIVATED */
			} else {
				/* To do: CARRIER_DEACTIVATED */
			}

			if (acmInfo->dteStatus & USB_DEVICE_CDC_CONTROL_SIG_BITMAP_DTE_PRESENCE) {
				/* DTE_ACTIVATED */
				if (1 == s_cdcVcom.attach) {

#if defined(FSL_FEATURE_USB_KHCI_KEEP_ALIVE_ENABLED) && (FSL_FEATURE_USB_KHCI_KEEP_ALIVE_ENABLED > 0U) && \
    defined(USB_DEVICE_CONFIG_KEEP_ALIVE_MODE) && (USB_DEVICE_CONFIG_KEEP_ALIVE_MODE > 0U) &&             \
    defined(FSL_FEATURE_USB_KHCI_USB_RAM) && (FSL_FEATURE_USB_KHCI_USB_RAM > 0U)
					s_waitForDataReceive = 1;
					USB0->INTEN &= ~USB_INTEN_SOFTOKEN_MASK;
					s_comOpen = 1;
					usb_echo("USB_APP_CDC_DTE_ACTIVATED\r\n");
#endif
				}

			} else {
				/* DTE_DEACTIVATED */
				if (1 == s_cdcVcom.attach) {
				}
			}
		}
		break;

	case kUSB_DeviceCdcEventSendBreak:
		break;

	default:
		break;
	}

	return error;
}

/*!
 * @brief USB device callback function.
 *
 * This function handles the usb device specific requests.
 *
 * @param handle          The USB device handle.
 * @param event           The USB device event type.
 * @param param           The parameter of the device specific request.
 *
 * @return A USB error code or kStatus_USB_Success.
 */
usb_status_t USB_DeviceCallback(usb_device_handle handle, uint32_t event, void *param)
{
	usb_status_t error = kStatus_USB_Error;
	uint16_t *temp16 = (uint16_t *)param;
	uint8_t *temp8 = (uint8_t *)param;

	switch (event) {
	case kUSB_DeviceEventBusReset: {
			s_cdcVcom.attach = 0;
		}
		break;

	case kUSB_DeviceEventSetConfiguration:
		if (param) {
			s_cdcVcom.attach = 1;
			s_cdcVcom.currentConfiguration = *temp8;

			if (USB_CDC_VCOM_CONFIGURE_INDEX == (*temp8)) {
				/* Schedule buffer for receive */
				USB_DeviceCdcAcmRecv(s_cdcVcom.cdcAcmHandle, USB_CDC_VCOM_BULK_OUT_ENDPOINT, s_currRecvBuf,
						     g_UsbDeviceCdcVcomDicEndpoints[0].maxPacketSize);
			}
		}

		break;

	case kUSB_DeviceEventSetInterface:
		if (s_cdcVcom.attach) {
			uint8_t interface = (uint8_t)((*temp16 & 0xFF00U) >> 0x08U);
			uint8_t alternateSetting = (uint8_t)(*temp16 & 0x00FFU);

			if (interface < USB_CDC_VCOM_INTERFACE_COUNT) {
				s_cdcVcom.currentInterfaceAlternateSetting[interface] = alternateSetting;
			}
		}

		break;

	case kUSB_DeviceEventGetConfiguration:
		break;

	case kUSB_DeviceEventGetInterface:
		break;

	case kUSB_DeviceEventGetDeviceDescriptor:
		if (param) {
			error = USB_DeviceGetDeviceDescriptor(handle, (usb_device_get_device_descriptor_struct_t *)param);
		}

		break;

	case kUSB_DeviceEventGetConfigurationDescriptor:
		if (param) {
			error = USB_DeviceGetConfigurationDescriptor(handle,
					(usb_device_get_configuration_descriptor_struct_t *)param);
		}

		break;

	case kUSB_DeviceEventGetStringDescriptor:
		if (param) {
			/* Get device string descriptor request */
			error = USB_DeviceGetStringDescriptor(handle, (usb_device_get_string_descriptor_struct_t *)param);
		}

		break;

	default:
		break;
	}

	return error;
}

/*!
 * @brief USB Interrupt service routine.
 *
 * This function serves as the USB interrupt service routine.
 *
 * @return None.
 */
#if defined(USB_DEVICE_CONFIG_KHCI) && (USB_DEVICE_CONFIG_KHCI > 0U)
void USB0_IRQHandler(void)
{
	USB_DeviceKhciIsrFunction(s_cdcVcom.deviceHandle);
}
#endif

void usb_cinit(void)
{
	uint8_t irqNo;
#if defined(USB_DEVICE_CONFIG_KHCI) && (USB_DEVICE_CONFIG_KHCI > 0)
	uint8_t khciIrq[] = USB_IRQS;
	irqNo = khciIrq[CONTROLLER_ID - kUSB_ControllerKhci0];

	SystemCoreClockUpdate();

	CLOCK_EnableUsbfs0Clock(USB_FS_CLK_SRC, USB_FS_CLK_FREQ);
#endif

	/*
	* If the SOC has USB KHCI dedicated RAM, the RAM memory needs to be clear after
	* the KHCI clock is enabled. When the demo uses USB EHCI IP, the USB KHCI dedicated
	* RAM can not be used and the memory can't be accessed.
	*/
#if (defined(FSL_FEATURE_USB_KHCI_USB_RAM) && (FSL_FEATURE_USB_KHCI_USB_RAM > 0U))
#if (defined(FSL_FEATURE_USB_KHCI_USB_RAM_BASE_ADDRESS) && (FSL_FEATURE_USB_KHCI_USB_RAM_BASE_ADDRESS > 0U))

	for (int i = 0; i < FSL_FEATURE_USB_KHCI_USB_RAM; i++) {
		((uint8_t *)FSL_FEATURE_USB_KHCI_USB_RAM_BASE_ADDRESS)[i] = 0x00U;
	}

#endif /* FSL_FEATURE_USB_KHCI_USB_RAM_BASE_ADDRESS */
#endif /* FSL_FEATURE_USB_KHCI_USB_RAM */

	s_cdcVcom.speed = USB_SPEED_FULL;
	s_cdcVcom.attach = 0;
	s_cdcVcom.cdcAcmHandle = (class_handle_t)NULL;
	s_cdcVcom.deviceHandle = NULL;

	if (kStatus_USB_Success != USB_DeviceClassInit(CONTROLLER_ID, &s_cdcAcmConfigList, &s_cdcVcom.deviceHandle)) {
		usb_echo("USB device init failed\r\n");

	} else {
		usb_echo("USB device CDC virtual com demo\r\n");
		s_cdcVcom.cdcAcmHandle = s_cdcAcmConfigList.config->classHandle;
	}

	NVIC_SetPriority((IRQn_Type)irqNo, USB_DEVICE_INTERRUPT_PRIORITY);
	NVIC_EnableIRQ((IRQn_Type)irqNo);

	USB_DeviceRun(s_cdcVcom.deviceHandle);
	g_usb_initalized = true;
}

void
usb_cfini(void)
{

	if (g_usb_initalized) {
		uint8_t irqNo;
		uint8_t khciIrq[] = USB_IRQS;
		irqNo = khciIrq[CONTROLLER_ID - kUSB_ControllerKhci0];

		NVIC_DisableIRQ((IRQn_Type)irqNo);
		USB_DeviceClassDeinit(CONTROLLER_ID);

		SIM->SOPT2 &= ~SIM_SOPT2_USBSRC_MASK;
		USB0->CLK_RECOVER_IRC_EN = 0x01U;
		USB0->CLK_RECOVER_CTRL = 0;


		CLOCK_DisableClock(kCLOCK_Usbfs0);
	}
}

int usb_cin(void)
{
	if ((!s_cdcVcom.attach) /*|| (!s_cdcVcom.startTransactions)*/) { return -1; }

	return buf_get();
}

void
usb_cout(uint8_t *buf, unsigned count)
{
	static uint8_t abuff[DATA_BUFF_SIZE] USB_DATA_ALIGN;

	for (int i = 0; i < count; i++) {
		abuff[i] = buf[i];
	}

	while (count) {
		unsigned len = (count > 64) ? 64 : count;

		while (!g_usb_tx_done) {};

		g_usb_tx_done = false;

		while (kStatus_USB_Busy == USB_DeviceCdcAcmSend(s_cdcVcom.cdcAcmHandle, USB_CDC_VCOM_BULK_IN_ENDPOINT, abuff, len));

		count -= len;
		buf += len;
	}

	while (!g_usb_tx_done) {};

}
